﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SPListRepository.cs" company="Collaboris Ltd.">
//   Copyright (c) Collaboris Ltd. All rights Reserved.
// </copyright>
// <summary>
//   Generic data access class to read and write information to a SharePoint list
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Collaboris.DataAccess.SharePoint
{
    #region Imports

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data;
    using System.IO;
    using System.Text;

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.Utilities;



    #endregion

    /// <summary>
    /// Generic data access class to read and write information to a SharePoint list
    /// </summary>
    /// <typeparam name="T">
    /// The Data entity Type
    /// </typeparam>
    public abstract class SPListRepository<T> : IRepository<T, SPCriteria>
        where T : DataEntityBase, new()
    {
        #region Constants and Fields

        /// <summary>
        /// The standard SP author field name
        /// </summary>
        private const string AuthorFieldName = "Author";

        /// <summary>
        /// The standard SP EditorFieldName
        /// </summary>
        private const string EditorFieldName = "Editor";

        /// <summary>
        /// The name of the list encapsulated by this repoeitory
        /// </summary>
        private readonly string listName;

        /// <summary>
        /// The share point list.
        /// </summary>
        private readonly SPList sharePointList;

        /// <summary>
        /// The web.
        /// </summary>
        private readonly SPWeb web;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="SPListRepository{T}"/> class. 
        /// Initializes a new instance of the <see cref="SPListRepository{T,C}"/> class. 
        /// Creates a Repository class to access the given list. It's assumed that the given list
        /// is located at the root of the site collection
        /// </summary>
        /// <param name="listName">
        /// </param>
        protected SPListRepository(string listName)
        {
            if (listName == null)
            {
                throw new ArgumentNullException("listName", "listName parameter cannot be null");
            }

            // Web has not been passed in so we assume that the list will be contained 
            // on the rootweb
            this.web = SPContext.Current.Site.RootWeb;
            this.listName = listName;

            // web.AllowUnsafeUpdates = true;
            this.sharePointList = SPListHelper.GetList(this.web, this.listName);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SPListRepository{T}"/> class. 
        /// Initializes a new instance of the <see cref="SPListRepository{T,C}"/> class. 
        /// Creates a Repository class to access the given list in the given web.
        /// </summary>
        /// <param name="web">
        /// </param>
        /// <param name="listName">
        /// </param>
        protected SPListRepository(SPWeb web, string listName)
        {
            if (listName == null)
            {
                throw new ArgumentNullException("listName", "listName parameter cannot be null");
            }

            if (web == null)
            {
                throw new ArgumentNullException("web", "web parameter cannot be null ");
            }

            this.web = web;
            this.listName = listName;

            this.sharePointList = this.web.Lists[this.listName];

            if (this.sharePointList == null)
            {
                throw new ApplicationException(
                    String.Format("Could not get instance list: '{0}' in site: '{1}'", listName, this.web.Url));
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SPListRepository{T}"/> class. 
        /// Initializes a new instance of the <see cref="SPListRepository{T,C}"/> class. 
        /// Creates a Repository class to access the given list in the given web.
        /// </summary>
        /// <param name="web">
        /// </param>
        /// <param name="list">
        /// </param>
        protected SPListRepository(SPWeb web, SPList list)
        {
            if (list == null)
            {
                throw new ArgumentNullException("list", "list parameter cannot be null");
            }

            if (web == null)
            {
                throw new ArgumentNullException("web", "web parameter cannot be null ");
            }

            this.sharePointList = list;

            this.listName = this.SharePointList.Title;

            this.web = web;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Returns the library's relative path
        /// </summary>
        public string LibraryRelativePath
        {
            get
            {
                if (this.SharePointList == null)
                {
                    return null;
                }

                return this.SharePointList.RootFolder.ServerRelativeUrl;
            }
        }

        /// <summary>
        /// The name of the SharePoint list
        /// </summary>
        public string ListName
        {
            get
            {
                return this.listName;
            }
        }

        /// <summary>
        /// The instance of the list that this class has access to.
        /// </summary>
        protected SPList SharePointList
        {
            get
            {
                return this.sharePointList;
            }
        }

        /// <summary>
        /// The instance of the SPWeb where the list is located
        /// </summary>
        protected SPWeb Web
        {
            get
            {
                return this.web;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns a data entity from a list item.
        /// </summary>
        /// <param name="listItem">
        /// </param>
        /// <returns>
        /// </returns>
        public virtual T CreateDataEntity(SPListItem listItem)
        {
            var dataEntity = new T();

            this.DataMap(listItem, ref dataEntity);

            dataEntity.Id = listItem.ID;

            return dataEntity;
        }

        /// <summary>
        /// The delete all items.
        /// </summary>
        public virtual void DeleteAllItems()
        {
            DeleteAllItems(this.SharePointList);
        }


        #endregion

        #region Implemented Interfaces

        #region IRepository<T,SPCriteria>

        /// <summary>
        /// The create criteria.
        /// </summary>
        /// <returns>
        /// </returns>
        public virtual SPCriteria CreateCriteria()
        {
            return new SPCriteria();
        }

        /// <summary>
        /// Returns a new empty data entity.
        /// </summary>
        /// <returns>
        /// New data entity
        /// </returns>
        public virtual T CreateDataEntity()
        {
            var dataEntity = new T();

            return dataEntity;
        }

        /// <summary>
        /// The delete.
        /// </summary>
        /// <param name="criteria">
        /// The criteria.
        /// </param>
        public virtual void Delete(SPCriteria criteria)
        {
            Delete(criteria.QueryExpression);
        }

        /// <summary>
        /// Deletes a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="itemId">
        /// The item Id.
        /// </param>
        public virtual void Delete(long itemId)
        {
            if (itemId <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    "itemId", "Business object ID has to be positive and bigger than zero");
            }

            this.web.AllowUnsafeUpdates = true;

            SPListItem listItem = this.SharePointList.GetItemById((int)itemId);

            listItem.Delete();
        }

        /// <summary>
        /// Returns the first business object that conforms to the criteria
        /// </summary>
        /// <param name="criteria">
        /// The criteria.
        /// </param>
        /// <returns>
        /// </returns>
        public virtual T Load(SPCriteria criteria)
        {
            if (criteria == null)
            {
                throw new ArgumentNullException("criteria");
            }

            if (criteria.QueryExpression == null)
            {
                throw new ArgumentException("Query expression cannot be null");
            }

            // If we have specified an id on the criteria then it means that
            // we want to load the item with that id.
            if (criteria.Id != 0)
            {
                return Load(criteria.Id);
            }

            SPQuery query = this.GetQuery(criteria);

            List<T> queryResults = this.LoadList(query, criteria.ViewName);

            if (queryResults.Count == 0)
            {
                return default(T);
            }

            return queryResults[0];
        }

        /// <summary>
        /// The load.
        /// </summary>
        /// <param name="itemId">
        /// </param>
        /// <returns>
        /// </returns>
        public virtual T Load(long itemId)
        {
            if (itemId == 0)
            {
                throw new ArgumentOutOfRangeException("itemId");
            }

            SPListItem listItem = this.GetListItem(itemId);
            var businessObject = new T();
            businessObject.Id = listItem.ID;
            DataMap(listItem, ref businessObject);

            return businessObject;
        }

        /// <summary>
        /// Returns the first business object that conforms to the criteria
        /// </summary>
        /// <remarks>
        /// Use with caution because it can return a big amout of data.
        /// </remarks>
        /// <returns>
        /// All the business objects in the data source
        /// </returns>
        public List<T> LoadAllList()
        {
            if (this.SharePointList == null)
            {
                return null;
            }

            if (this.SharePointList.Items == null)
            {
                return null;
            }

            return this.LoadList(new SPQuery(), null);
        }

        /// <summary>
        /// Returns a data table with rows that conform to the criteria
        /// </summary>
        /// <returns>
        /// </returns>
        public virtual DataTable LoadAllTable()
        {
            if (this.SharePointList == null)
            {
                return null;
            }

            if (this.SharePointList.Items == null)
            {
                return null;
            }

            return this.SharePointList.Items.GetDataTable();
        }

        /// <summary>
        /// Returns a collection of business objects that conform to the criteria
        /// </summary>
        /// <param name="criteria">
        /// The criteria.
        /// </param>
        /// <returns>
        /// </returns>
        public virtual List<T> LoadList(SPCriteria criteria)
        {
            if (criteria == null)
            {
                throw new ArgumentNullException("criteria");
            }

            SPQuery query = this.GetQuery(criteria);

            return this.LoadList(query, criteria.ViewName);
        }

        /// <summary>
        /// Returns a data table with rows that conform to the criteria
        /// </summary>
        /// <param name="criteria">
        /// </param>
        /// <returns>
        /// </returns>
        public virtual DataTable LoadTable(SPCriteria criteria)
        {
            if (criteria == null)
            {
                throw new ArgumentNullException("criteria");
            }

            SPQuery query = this.GetQuery(criteria);

            SPListItemCollection listItemCollection = this.ExecuteQuery(query, criteria.ViewName);

            if (listItemCollection == null)
            {
                return null;
            }

            return listItemCollection.GetDataTable();
        }

        /// <summary>
        /// Submits the specified data items.
        /// </summary>
        /// <param name="dataItems">
        /// The data items.
        /// </param>
        /// <returns>
        /// </returns>
        public virtual List<long> Submit(List<T> dataItems)
        {
            var submittedIds = new List<long>();

            foreach (T dataItem in dataItems)
            {
                submittedIds.Add(this.Submit(dataItem));
            }

            return submittedIds;
        }

        /// <summary>
        /// Saves the list item in the Sharepoint list.
        /// If the given list item contains an ID a method with the same ID will be updated
        /// in sharepoint, otherwise a new item is inserted in the list.
        /// </summary>
        /// <param name="dataItem">
        /// </param>
        /// <returns>
        /// The Id of the item
        /// </returns>
        public virtual long Submit(T dataItem)
        {
            SPListItem listItem;

            this.web.AllowUnsafeUpdates = true;

            // Check if the business object already has an ID
            // if it does the  we need to find an SPListItem with the same
            // id and update it
            if (dataItem.Id != 0)
            {
                listItem = this.SharePointList.GetItemById((int)dataItem.Id);
            }
            else
            {
                listItem = this.SharePointList.Items.Add();
            }

            DataMap(dataItem, ref listItem);

            listItem.Update();

            // Update the business object with the id of the item just saved
            dataItem.Id = listItem.ID;

            return listItem.ID;
        }

        #endregion

        #endregion

        #region Helper Methods

        /// <summary>
        /// Builds a batch string with a list of all the items that are to be deleted.
        /// </summary>
        /// <param name="list">
        /// The list to delete all items from.
        /// </param>
        /// <returns>
        /// </returns>
        protected static StringBuilder BuildBatchDeleteCommand(SPList list)
        {
            StringBuilder sbDelete = new StringBuilder();
            sbDelete.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            sbDelete.Append("<Batch>");
            string command = "<Method><SetList Scope=\"Request\">" + list.ID +
                             "</SetList><SetVar Name=\"ID\">{0}</SetVar><SetVar Name=\"Cmd\">Delete</SetVar></Method>";
            foreach (SPListItem item in list.Items)
            {
                sbDelete.Append(string.Format(command, item.ID.ToString()));
            }

            sbDelete.Append("</Batch>");
            return sbDelete;
        }

        /// <summary>
        /// Deletes all the items from the specifid list. Uses batch processing for performance.
        /// </summary>
        /// <param name="list">
        /// The name of the list that has the items to be deleted.
        /// </param>
        protected static void DeleteAllItems(SPList list)
        {
            using (SPSite site = list.ParentWeb.Site)
            {
                StringBuilder sbDelete = BuildBatchDeleteCommand(list);

                site.RootWeb.ProcessBatchData(sbDelete.ToString());
            }
        }

        /// <summary>
        /// Converts a business object into a HashTable. 
        /// Required in order to be able to upload documents to document libraries.
        /// Can be disregarded for custom lists.
        /// </summary>
        /// <param name="businessObject">
        /// </param>
        /// <param name="hashTable">
        /// </param>
        protected virtual void DataMap(T businessObject, ref Hashtable hashTable)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Converts a SP list item into a business object
        /// To be overriden by derived classes
        /// </summary>
        /// <param name="srcListItem">
        /// The src List Item.
        /// </param>
        /// <param name="dstBusinessObject">
        /// The dst Business Object.
        /// </param>
        protected abstract void DataMap(SPListItem srcListItem, ref T dstBusinessObject);

        /// <summary>
        /// Converts business object into an SPListItem
        /// To be overriden by derived classes
        /// </summary>
        /// <param name="srcBusinessObject">
        /// The src Business Object.
        /// </param>
        /// <param name="dstListItem">
        /// The dst List Item.
        /// </param>
        protected abstract void DataMap(T srcBusinessObject, ref SPListItem dstListItem);

        /// <summary>
        /// Deletes a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="queryExpression">
        /// The query Expression.
        /// </param>
        protected virtual void Delete(QueryExpression queryExpression)
        {
            if (queryExpression == null)
            {
                throw new ArgumentNullException("queryExpression");
            }

            var spQueryBuilder = new SPQueryBuilder();
            SPQuery query = spQueryBuilder.BuildQuery(queryExpression);

            Delete(query);
        }

        /// <summary>
        /// Deletes a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="queryParam">
        /// The query Param.
        /// </param>
        protected virtual void Delete(QueryParam queryParam)
        {
            this.Delete(new QueryExpression(queryParam));
        }

        /// <summary>
        /// Deletes a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        protected virtual void Delete(SPQuery query)
        {
            if (query == null)
            {
                throw new ArgumentNullException("query");
            }

            this.web.AllowUnsafeUpdates = true;

            SPListItemCollection listItemCollection = this.ExecuteQuery(query, null);

            // Delete each item
            while (listItemCollection.Count > 0)
            {
                listItemCollection.Delete(0);
            }
            /*
            foreach (SPListItem item in listItemCollection)
            {
                item.Delete();
            }*/
        }

        /// <summary>
        /// The execute query.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <param name="viewName">
        /// The view name.
        /// </param>
        /// <returns>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// </exception>
        protected SPListItemCollection ExecuteQuery(SPQuery query, string viewName)
        {
            if (query == null)
            {
                throw new ArgumentNullException(
                    "query", "Query object cannot be null in call to SPListRepository::ExecuteQuery");
            }

            Guid listGuid;
            SPListItemCollection queryResults;

            if (!String.IsNullOrEmpty(viewName))
            {
                SPView view = this.sharePointList.Views[viewName];
                listGuid = view.ID;

                queryResults = this.sharePointList.GetItems(query, listGuid.ToString("B").ToUpper());
            }
            else
            {
                queryResults = this.sharePointList.GetItems(query);
            }

            return queryResults;
        }

        /// <summary>
        /// The get list item.
        /// </summary>
        /// <param name="itemId">
        /// The item id.
        /// </param>
        /// <returns>
        /// </returns>
        protected SPListItem GetListItem(long itemId)
        {
            return this.sharePointList.GetItemById((int)itemId);
        }

        /// <summary>
        /// The get query.
        /// </summary>
        /// <param name="criteria">
        /// The criteria.
        /// </param>
        /// <returns>
        /// </returns>
        protected virtual SPQuery GetQuery(SPCriteria criteria)
        {
            var spQueryBuilder = new SPQueryBuilder();
            spQueryBuilder.MaxRows = criteria.RowLimit;
            spQueryBuilder.SortParamCollection = criteria.SortParamCollection;

            QueryColumnCollection queryColumns = null;
            if (criteria.Columns.Count > 0)
            {
                queryColumns = criteria.Columns;
            }
            else
            {
                queryColumns = new QueryColumnCollection();
                this.SetListColumns(queryColumns);
            }

            spQueryBuilder.Columns = queryColumns;

            return spQueryBuilder.BuildQuery(criteria.QueryExpression);
        }

        /// <summary>
        /// Gets the number of items that conform to the query
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <returns>
        /// The get row count.
        /// </returns>
        protected virtual int GetRowCount(SPQuery query)
        {
            // Todo: Need to find a better way to do this
            SPListItemCollection queryResults = this.SharePointList.GetItems(query);

            return queryResults.Count;
        }

        /// <summary>
        /// Gets a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <param name="viewName">
        /// The view Name.
        /// </param>
        /// <returns>
        /// </returns>
        protected virtual List<T> LoadList(SPQuery query, string viewName)
        {
            if (query == null)
            {
                throw new ArgumentNullException("query");
            }

            SPListItemCollection listItemCollection = this.ExecuteQuery(query, viewName);

            var businessList = new List<T>();

            // Create business object list from SPListItem list
            foreach (SPListItem item in listItemCollection)
            {
                var businessObject = new T();

                // iterate through all the 
                this.DataMap(item, ref businessObject);

                // set the id
                businessObject.Id = item.ID;

                businessList.Add(businessObject);
            }

            return businessList;
        }

        /// <summary>
        /// Gets a collection of Sharepoint list Items that conform to the 
        /// criteria.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <returns>
        /// </returns>
        protected virtual List<T> LoadList(SPQuery query)
        {
            return this.LoadList(query, null);
        }

        /// <summary>
        /// Espcifies the columns to be used by the query
        /// </summary>
        /// <param name="columns">
        /// </param>
        protected abstract void SetListColumns(QueryColumnCollection columns);

        /// <summary>
        /// Saves the list item in the Sharepoint list.
        /// If the given list item contains an ID a method with the same ID will be updated
        /// in sharepoint, otherwise a new item is inserted in the list.
        /// </summary>
        /// <param name="businessObject">
        /// </param>
        /// <param name="user">
        /// </param>
        /// <returns>
        /// The Id of the item
        /// </returns>
        protected virtual long Submit(T businessObject, SPUser user)
        {
            SPListItem listItem;

            this.web.AllowUnsafeUpdates = true;

            // Check if the business object already has an ID
            // if it does the  we need to find an SPListItem with the same id and update it
            if (businessObject.Id != 0)
            {
                listItem = this.SharePointList.GetItemById((int)businessObject.Id);
            }
            else
            {
                listItem = this.SharePointList.Items.Add();

                // This is a new entry so we need to set the name of the author
                SPListHelper.SetColumnValue(listItem, AuthorFieldName, user);
            }

            // Set the name of the editor
            SPListHelper.SetColumnValue(listItem, EditorFieldName, user);

            this.DataMap(businessObject, ref listItem);

            listItem.Update();

            // Update the business object with the id of the item just saved
            businessObject.Id = listItem.ID;

            return listItem.ID;
        }

        #endregion
    }
}
